diff --git a/swh/web/ui/apidoc.py b/swh/web/ui/apidoc.py
--- a/swh/web/ui/apidoc.py
+++ b/swh/web/ui/apidoc.py
@@ -3,47 +3,236 @@
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import re
+
+from functools import wraps
+
+from flask import request, render_template, url_for
+from flask import g
 
-import os
-from swh.web.ui import utils, main
 from swh.web.ui.main import app
 
 
-def _create_url_doc_endpoints(rules):
-    def split_path(path, acc):
-        rpath = os.path.dirname(path)
-        if rpath == '/':
-            yield from acc
-        else:
-            acc.append(rpath+'/')
-            yield from split_path(rpath, acc)
-
-    url_doc_endpoints = set()
-    for rule in rules:
-        url_rule = rule['rule']
-        url_doc_endpoints.add(url_rule)
-        if '<' in url_rule or '>' in url_rule:
-            continue
-        acc = []
-        for rpath in split_path(url_rule, acc):
-            if rpath in url_doc_endpoints:
-                continue
-            yield rpath
-            url_doc_endpoints.add(rpath)
-
-
-def install_browsable_api_endpoints():
-    """Install browsable endpoints.
-
-    """
-    url_doc_endpoints = _create_url_doc_endpoints(main.rules())
-    for url_doc in url_doc_endpoints:
-        endpoint_name = 'doc_api_' + url_doc.strip('/').replace('/', '_')
-
-        def view_func(url_doc=url_doc):
-            return utils.filter_endpoints(main.rules(),
-                                          url_doc)
-        app.add_url_rule(rule=url_doc,
-                         endpoint=endpoint_name,
-                         view_func=view_func,
-                         methods=['GET'])
+class argtypes(object):
+    """Class for centralizing argument type descriptions
+
+    """
+
+    ts = 'timestamp'
+    int = 'integer'
+    path = 'path'
+    sha1 = 'sha1'
+    uuid = 'uuid'
+    sha1_git = 'sha1_git'
+    octet_stream = 'octet stream'
+    algo_and_hash = 'algo_hash:hash'
+
+
+class rettypes(object):
+    """Class for centralizing return type descriptions
+
+    """
+    list = 'list'
+    dict = 'dict'
+
+
+class excs(object):
+    """Class for centralizing exception type descriptions
+
+    """
+
+    badinput = 'BadInputExc'
+    notfound = 'NotFoundExc'
+
+
+class APIUrls(object):
+    """
+    Class to manage API documentation URLs.
+      * Indexes all routes documented using apidoc's decorators.
+      * Tracks endpoint/request processing method relationships for use
+        in generating related urls in API documentation
+    Relies on the load_controllers logic in main.py for initialization.
+
+    """
+    apidoc_routes = {}
+    method_endpoints = {}
+
+    @classmethod
+    def get_app_endpoints(cls):
+        return cls.apidoc_routes
+
+    @classmethod
+    def get_method_endpoints(cls, fname):
+        if len(cls.method_endpoints) == 0:
+            cls.method_endpoints = cls.group_routes_by_method()
+        return cls.method_endpoints[fname]
+
+    @classmethod
+    def group_routes_by_method(cls):
+        """
+        Group URL endpoints according to their processing method.
+        Returns:
+            A dict where keys are the processing method names, and values
+            are the routes that are bound to the key method.
+        """
+        endpoints = {}
+        for rule in app.url_map.iter_rules():
+            rule_dict = {'rule': rule.rule,
+                         'methods': rule.methods}
+            if rule.endpoint not in endpoints:
+                endpoints[rule.endpoint] = [rule_dict]
+            else:
+                endpoints[rule.endpoint].append(rule_dict)
+        return endpoints
+
+    @classmethod
+    def index_add_route(cls, route, docstring):
+        """
+        Add a route to the self-documenting API reference
+        """
+        if route not in cls.apidoc_routes:
+            cls.apidoc_routes[route] = docstring
+
+
+class route(object):
+    """
+    Decorate an API method to register it in the API doc route index
+    and create the corresponding Flask route.
+    Caution: decorating a method with this requires to also decorate it
+    __at least__ with @returns, or breaks the decorated endpoint
+    Args:
+        route: the documentation page's route
+        noargs: set to True if the route has no arguments, and its result
+        should be displayed anytime its documentation is requested
+    """
+    def __init__(self, route, noargs=False):
+        self.route = route
+        self.noargs = noargs
+
+    def __call__(self, f):
+        APIUrls.index_add_route(self.route, f.__doc__)
+
+        @wraps(f)
+        def doc_func(*args, **kwargs):
+            return f(call_args=(args, kwargs),
+                     doc_route=self.route,
+                     noargs=self.noargs)
+
+        if not self.noargs:
+            app.add_url_rule(self.route, f.__name__, doc_func)
+
+        return doc_func
+
+
+class arg(object):
+    """
+    Decorate an API method to display an argument's information on the doc
+    page specified by @route above.
+    Args:
+        name: the argument's name. MUST match the method argument's name to
+        create the example request URL.
+        default: the argument's default value
+        argtype: the argument's type (map, dict, list, tuple...)
+        argdoc: the argument's documentation string
+    """
+    def __init__(self, name, default, argtype, argdoc):
+        self.doc_dict = {
+            'name': name,
+            'type': argtype,
+            'doc': argdoc,
+            'default': default
+        }
+
+    def __call__(self, f):
+        @wraps(f)
+        def arg_fun(*args, **kwargs):
+            if 'args' in kwargs:
+                kwargs['args'].append(self.doc_dict)
+            else:
+                kwargs['args'] = [self.doc_dict]
+            return f(*args, **kwargs)
+        return arg_fun
+
+
+class raises(object):
+    """
+    Decorate an API method to display information pertaining to an exception
+    that can be raised by this method.
+    Args:
+        exc: the exception name
+        doc: the exception's documentation string
+    """
+    def __init__(self, exc, doc):
+        self.exc_dict = {
+            'exc': exc,
+            'doc': doc
+        }
+
+    def __call__(self, f):
+        @wraps(f)
+        def exc_fun(*args, **kwargs):
+            if 'excs' in kwargs:
+                kwargs['excs'].append(self.exc_dict)
+            else:
+                kwargs['excs'] = [self.exc_dict]
+            return f(*args, **kwargs)
+        return exc_fun
+
+
+class returns(object):
+    """
+    Decorate an API method to display information about its return value.
+    Caution: this MUST be the last decorator in the apidoc decorator stack,
+    or the decorated endpoint breaks
+    Args:
+        rettype: the return value's type (map, dict, list, tuple...)
+        retdoc: the return value's documentation string
+    """
+    def __init__(self, rettype=None, retdoc=None):
+        self.return_dict = {
+            'type': rettype,
+            'doc': retdoc
+        }
+
+    def filter_api_url(self, endpoint, route_re, noargs):
+        doc_methods = {'GET', 'HEAD', 'OPTIONS'}
+        if re.match(route_re, endpoint['rule']):
+            if endpoint['methods'] == doc_methods and not noargs:
+                return False
+        return True
+
+    def __call__(self, f):
+        @wraps(f)
+        def ret_fun(*args, **kwargs):
+            # Build documentation
+            env = {
+                'docstring': f.__doc__,
+                'route': kwargs['doc_route'],
+                'return': self.return_dict
+            }
+
+            for arg in ['args', 'excs']:
+                if arg in kwargs:
+                    env[arg] = kwargs[arg]
+
+            route_re = re.compile('.*%s$' % kwargs['doc_route'])
+            endpoint_list = APIUrls.get_method_endpoints(f.__name__)
+            other_urls = [url for url in endpoint_list if
+                          self.filter_api_url(url, route_re, kwargs['noargs'])]
+            env['urls'] = other_urls
+
+            # Build example endpoint URL
+            if 'args' in env:
+                defaults = {arg['name']: arg['default'] for arg in env['args']}
+                env['example'] = url_for(f.__name__, **defaults)
+
+            # Prepare and send to mimetype selector if it's not a doc request
+            if re.match(route_re, request.url) and not kwargs['noargs']:
+                return app.response_class(
+                    render_template('apidoc.html', **env),
+                    content_type='text/html')
+
+            cargs, ckwargs = kwargs['call_args']
+            g.doc_env = env  # Store for response processing
+            return f(*cargs, **ckwargs)
+        return ret_fun
diff --git a/swh/web/ui/backend.py b/swh/web/ui/backend.py
--- a/swh/web/ui/backend.py
+++ b/swh/web/ui/backend.py
@@ -185,7 +185,7 @@
     return []
 
 
-def revision_log(sha1_git_bin, limit=100):
+def revision_log(sha1_git_bin, limit):
     """Return information about the revision with sha1 sha1_git_bin.
 
     Args:
@@ -202,7 +202,7 @@
     return main.storage().revision_log([sha1_git_bin], limit)
 
 
-def revision_log_by(origin_id, branch_name, ts, limit=100):
+def revision_log_by(origin_id, branch_name, ts, limit):
     """Return information about the revision matching the timestamp
     ts, from origin origin_id, in branch branch_name.
 
@@ -215,12 +215,10 @@
         Information for the revision matching the criterions.
 
     """
-    rev_list = main.storage().revision_log_by(origin_id,
-                                              branch_name,
-                                              ts)
-    if rev_list is None:
-        return None
-    return rev_list[:limit]
+    return main.storage().revision_log_by(origin_id,
+                                          branch_name,
+                                          ts,
+                                          limit=limit)
 
 
 def stat_counters():
diff --git a/swh/web/ui/converters.py b/swh/web/ui/converters.py
--- a/swh/web/ui/converters.py
+++ b/swh/web/ui/converters.py
@@ -197,8 +197,8 @@
     """Convert swh content to serializable content dictionary.
 
     """
-    if content and 'ctime' in content:
-        del content['ctime']
+    if content:
+        content = {k: v for k, v in content.items() if k not in ['ctime']}
     return from_swh(content,
                     hashess={'sha1', 'sha1_git', 'sha256'},
                     bytess={},
diff --git a/swh/web/ui/main.py b/swh/web/ui/main.py
--- a/swh/web/ui/main.py
+++ b/swh/web/ui/main.py
@@ -5,13 +5,15 @@
 
 import logging
 import os
+import json
 
-from flask.ext.api import FlaskAPI
+from flask import Flask
 from swh.core import config
 
 from swh.web.ui.renderers import RENDERERS, urlize_api_links
 from swh.web.ui.renderers import safe_docstring_display
 from swh.web.ui.renderers import revision_id_from_url
+from swh.web.ui.renderers import SWHMultiResponse
 from swh.storage import get_storage
 
 
@@ -26,15 +28,13 @@
     'max_log_revs': ('int', 25),
 }
 
-
 # api's definition
-app = FlaskAPI(__name__)
+app = Flask(__name__)
+app.response_class = SWHMultiResponse
 app.jinja_env.filters['urlize_api_links'] = urlize_api_links
 app.jinja_env.filters['safe_docstring_display'] = safe_docstring_display
 app.jinja_env.filters['revision_id_from_url'] = revision_id_from_url
 
-AUTODOC_ENDPOINT_INSTALLED = False
-
 
 def read_config(config_file):
     """Read the configuration file `config_file`, update the app with
@@ -54,12 +54,6 @@
     """
     from swh.web.ui import views, apidoc  # flake8: noqa
 
-    # side-effects here (install autodoc endpoints so do it only once!)
-    global AUTODOC_ENDPOINT_INSTALLED
-    if not AUTODOC_ENDPOINT_INSTALLED:
-        apidoc.install_browsable_api_endpoints()
-        AUTODOC_ENDPOINT_INSTALLED = True
-
 
 def rules():
     """Returns rules from the application in dictionary form.
diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py
--- a/swh/web/ui/renderers.py
+++ b/swh/web/ui/renderers.py
@@ -5,8 +5,10 @@
 
 import re
 import yaml
+import json
 
-from flask import make_response, request
+from flask import make_response, request, Response, render_template
+from flask import g
 from flask.ext.api import renderers, parsers
 from flask_api.mediatypes import MediaType
 from swh.web.ui import utils
@@ -80,6 +82,61 @@
         return self.enrich_with_jsonp(res)
 
 
+class SWHMultiResponse(Response, SWHFilterEnricher):
+    """
+    A Flask Response subclass.
+    Override force_type to transform dict responses into callable Flask
+    response objects whose mimetype matches the request's Accept header: HTML
+    template render, YAML dump or default to a JSON dump.
+    """
+
+    @classmethod
+    def make_response_from_mimetype(cls, rv):
+
+        def wants_html(best_match):
+            return best_match == 'text/html' and \
+                request.accept_mimetypes[best_match] > \
+                request.accept_mimetypes['application/json']
+
+        def wants_yaml(best_match):
+            return best_match == 'application/yaml' and \
+                request.accept_mimetypes[best_match] > \
+                request.accept_mimetypes['application/json']
+
+        if isinstance(rv, dict) or isinstance(rv, list):
+            rv = cls.filter_by_fields(cls, rv)
+            acc_mime = ['application/json', 'application/yaml', 'text/html']
+            best_match = request.accept_mimetypes.best_match(acc_mime)
+            # return a template render
+            if wants_html(best_match):
+                data = json.dumps(rv, sort_keys=True,
+                                  indent=4, separators=(',', ': '))
+                env = g.get('doc_env', {})
+                env['response_data'] = data
+                env['request'] = request
+                rv = Response(render_template('apidoc.html', **env),
+                              content_type='text/html')
+            # return formatted yaml
+            elif wants_yaml(best_match):
+                rv = Response(
+                    yaml.dump(rv),
+                    content_type='application/yaml')
+            # return formatted json
+            else:
+                # jsonify is unhappy with lists in Flask 0.10.1, use json.dumps
+                rv = Response(
+                    json.dumps(rv),
+                    content_type='application/json')
+        return rv
+
+    @classmethod
+    def force_type(cls, rv, environ=None):
+        # Data from apidoc
+        if isinstance(rv, dict) or isinstance(rv, list):
+            rv = cls.make_response_from_mimetype(rv)
+        return super().force_type(rv, environ)
+
+
 def urlize_api_links(content):
     """Utility function for decorating api links in browsable api."""
     return re.sub(r'"(/api/.*|/browse/.*)"', r'"<a href="\1">\1</a>"', content)
diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -271,7 +271,7 @@
     return converters.from_revision(res)
 
 
-def lookup_revision_log(rev_sha1_git, limit=25):
+def lookup_revision_log(rev_sha1_git, limit):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
@@ -294,7 +294,7 @@
     return map(converters.from_revision, revision_entries)
 
 
-def lookup_revision_log_by(origin_id, branch_name, timestamp, limit=25):
+def lookup_revision_log_by(origin_id, branch_name, timestamp, limit):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
diff --git a/swh/web/ui/templates/api.html b/swh/web/ui/templates/api.html
--- a/swh/web/ui/templates/api.html
+++ b/swh/web/ui/templates/api.html
@@ -1,194 +1,13 @@
-<!DOCTYPE html>
-<html>
-    <head>
-    {% block head %}
-
-        {% block meta %}
-        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-        <meta name="robots" content="NONE,NOARCHIVE" />
-        {% endblock %}
-
-        <title>{% block title %}Software Heritage API{% endblock %}</title>
-
-        {% block style %}
-        {% block bootstrap_theme %}
-            <link rel="stylesheet" type="text/css" href="{{url_for('flask-api.static', filename='css/bootstrap.min.css')}}"/>
-            <link rel="stylesheet" type="text/css" href="{{url_for('flask-api.static', filename='css/bootstrap-tweaks.css')}}"/>
-        {% endblock %}
-        <link rel="stylesheet" type="text/css" href="{{url_for('flask-api.static', filename='css/prettify.css')}}"/>
-        <link rel="stylesheet" type="text/css" href="{{url_for('flask-api.static', filename='css/default.css')}}"/>
-        {% endblock %}
-
-    {% endblock %}
-    </head>
-
-  <body class="{% block bodyclass %}{% endblock %} container">
-
-    <div class="wrapper">
-
-    {% block navbar %}
-    <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
-        <div class="navbar-inner">
-            <div class="container-fluid">
-                <span href="/">
-                    {% block branding %}<a class="navbar-brand" rel="nofollow" href='{{url_for('homepage')}}'>Software Heritage API v1</span></a>{% endblock %}
-                </span>
-                <ul class="nav pull-right">
-                    {% block userlinks %}
-                        <!--{ if user.is_authenticated }
-                            <li class="dropdown">
-                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">
-                                    {{ user }}
-                                    <b class="caret"></b>
-                                </a>
-                                <ul class="dropdown-menu">
-                                    <li>optional_logout request</li>
-                                </ul>
-                            </li>
-                        { else }-->
-                            <li><!-- optional_login request --></li>
-                        <!--{ endif }-->
-                    {% endblock %}
-                </ul>
-            </div>
-        </div>
-    </div>
-    {% endblock %}
-
-    <!--
-    {% block breadcrumbs %}
-    <ul class="breadcrumb">
-        {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
-            <li>
-                <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
-            </li>
-        {% endfor %}
-    </ul>
-    {% endblock %}
-    -->
-    <div style="height: 50px"></div>
-
-    <!-- Content -->
-    <div id="content">
-
-        {% if 'GET' in allowed_methods %}
-            <form id="get-form" class="pull-right">
-                <fieldset>
-                    <div class="btn-group format-selection">
-                        <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
-
-                        <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
-                            <span class="caret"></span>
-                        </button>
-                        <ul class="dropdown-menu">
-                            {% for format in available_formats %}
-                                <li>
-                                    <a class="js-tooltip format-option" href='<!-- add_query_param request api_settings.URL_FORMAT_OVERRIDE format -->' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
-                                </li>
-                            {% endfor %}
-                        </ul>
-                    </div>
-
-                </fieldset>
-            </form>
-        {% endif %}
-
-        <!--{% if 'OPTIONS' in allowed_methods %}
-            <form class="button-form" action="{{ request.full_path }}" method="POST" class="pull-right">
-                <- csrf_token ->
-                <input type="hidden" name="_method" value="OPTIONS" />
-                <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the resource">OPTIONS</button>
-            </form>
-        {% endif %}-->
-
-        {% if 'DELETE' in allowed_methods %}
-            <form class="button-form" action="{{ request.full_path }}" method="POST" class="pull-right">
-                <!-- csrf_token -->
-                <input type="hidden" name="_method" value="DELETE" />
-                <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the resource">DELETE</button>
-            </form>
-        {% endif %}
-
-        <div class="content-main">
-            <div class="page-header"><h1>{{ view_name }}</h1></div>
-            {% if view_description %}
-            <div style="margin-top: -10px; margin-bottom: 10px">
-              {{ view_description | safe_docstring_display | safe}}
-            </div>
-            {% endif %}
-            <div class="request-info" style="clear: both" >
-                <pre><b>{{ request.method }}</b> {{ request.full_path }}</pre>
-            </div>
-            <div class="response-info">
-                <pre><div class="meta nocode"><b>HTTP {{ status }}</b>{% autoescape off %}
-{% for key, val in headers.items() %}<b>{{ key }}:</b> <span class="lit">{{ val|e }}<!--{ val|break_long_headers|urlize_quoted_links }--></span>
-{% endfor %}
-</div>{% if content %}{{ content|urlize_api_links }}{% endif %}<!-- |urlize_quoted_links --></pre>{% endautoescape %}
-            </div>
-        </div>
-
-
-                {% if 'POST' in allowed_methods or 'PUT' in allowed_methods or 'PATCH' in allowed_methods %}
-                <div>
-                    <div class="well">
-                        <div id="generic-content-form">
-                            <form action="{{ request.full_path }}" method="POST" class="form-horizontal">
-                                <fieldset>
-<div class="control-group">
-        <label for="id__content_type" class="control-label">Media type:</label>
-        <div class="controls">
-            <select id="id__content_type" name="_content_type">
-<option value="application/json" selected="selected">application/json</option>
-<option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option>
-<option value="multipart/form-data">multipart/form-data</option>
-</select>
-            <span class="help-block"></span>
-        </div>
-    </div>
-                                <div class="control-group">
-                                    <label for="id__content" class="control-label">Content:</label>
-                                    <div class="controls">
-                                        <textarea name="_content" cols="40" rows="10"></textarea>
-                                    </div>
-                                </div>
-                                <div class="form-actions">
-                                    {% if 'POST' in allowed_methods %}
-                                    <button class="btn btn-primary" title="Make a POST request on the resource">POST</button>
-                                    {% endif %}
-                                    {% if 'PUT' in allowed_methods %}
-                                    <button class="btn btn-primary js-tooltip" name="_method" value="PUT" title="Make a PUT request on the resource">PUT</button>
-                                    {% endif %}
-                                    {% if 'PATCH' in allowed_methods %}
-                                    <button class="btn btn-primary js-tooltip" name="_method" value="PATCH" title="Make a PATCH request on the resource">PATCH</button>
-                                    {% endif %}
-                                    </div>
-                                </fieldset>
-                            </form>
-                        </div>
-                    </div>
-                </div>
-                {% endif %}
-
-        </div>
-        <!-- END content-main -->
-
-    </div>
-    <!-- END Content -->
-
-        <div id="push"></div>
-
-    </div>
-
-    </div><!-- ./wrapper -->
-
-    {% block footer %}
-    {% endblock %}
-
-    {% block script %}
-    <script src="{{url_for('flask-api.static', filename='js/jquery.min.js')}}"></script>
-    <script src="{{url_for('flask-api.static', filename='js/bootstrap.min.js')}}"></script>
-    <script src="{{url_for('flask-api.static', filename='js/prettify-min.js')}}"></script>
-    <script src="{{url_for('flask-api.static', filename='js/default.js')}}"></script>
-    {% endblock %}
-  </body>
-</html>
+{% extends "layout.html" %}
+{% block title %}Software Heritage API Overview{% endblock %}
+{% block content %}
+<div class="api-doc">
+  {% for route, doc in doc_routes %}
+  <div class="api-doc-route">
+    <h2> <a href="{{ route }}">{{ route }}</a> </h2>
+    {{ doc }}
+  </div>
+  </br>
+  {% endfor %}
+</div>
+{% endblock %}
diff --git a/swh/web/ui/templates/apidoc.html b/swh/web/ui/templates/apidoc.html
new file mode 100644
--- /dev/null
+++ b/swh/web/ui/templates/apidoc.html
@@ -0,0 +1,84 @@
+{% extends "layout.html" %}
+{% block title %}Software Heritage API{% endblock %}
+{% block content %}
+
+{% if docstring %}
+<div class="docstring">
+  <h2> Overview </h2>
+  {{ docstring | safe }}
+</div>
+{% endif %}
+{% if response_data and response_data is not none %}
+<div class="response-data">
+  <h2> Request </h2>
+  <pre><strong>{{ request.method }}</strong> {{ request.url }}</pre>
+  <h2> Result </h2>
+  <pre> {% autoescape off %} {{ response_data | urlize_api_links }} {% endautoescape %} </pre>
+</div>
+{% endif %}
+<hr/>
+<div class="doc-urls">
+  <table class="m-x-auto table">
+    <thead>
+      <tr>
+	<th>URL</th>
+	<th>Allowed Methods</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for url in urls %}
+      <tr>
+	<td>
+	  {{ url['rule'] }}
+	</td>
+	<td>
+	  {{ url['methods'] | sort | join(', ') }}
+	</td>
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+</div>
+<hr/>
+{% if args and args|length > 0 %}
+<div class="doc-args">
+  <h2> Args </h2>
+  <dl class="doc-argslist dl-horizontal">
+    {% for arg in args %}
+    <dt> {{ arg['name'] }}: {{ arg['type'] }} </dt>
+    <dd> {{ arg['doc'] }} </dd>
+    {% endfor %}
+  </dl>
+</div>
+{% endif %}
+{% if excs and excs|length > 0 %}
+<div class="doc-excs">
+  <h2> Raises </h2>
+  <dl class="doc-excslist dl-horizontal">
+    {% for exc in excs %}
+    <dt> {{ exc['exc'] }} </dt>
+    <dd> {{ exc['doc'] }} </dd>
+    {% endfor %}
+  </dl>
+</div>
+{% endif %}
+{% if return %}
+<div class="doc-return">
+  <h2> Returns </h2>
+  <dl class="doc-return dl-horizontal">
+    <dt>{{ return['type'] }}</dt>
+    <dd>{{ return['doc'] }}</dd>
+  </dl>
+</div>
+{% endif %}
+{% if example %}
+<div class="doc-example">
+  <h2> Example </h2>
+  <dl class="doc-example dl-horizontal">
+    <dd>
+      <a href="{{ example }}">{{ example }}</a>
+    </dd>
+  </dl>
+</div>
+{% endif %}
+{% endblock %}
diff --git a/swh/web/ui/templates/origin.html b/swh/web/ui/templates/origin.html
--- a/swh/web/ui/templates/origin.html
+++ b/swh/web/ui/templates/origin.html
@@ -20,7 +20,7 @@
     </div>
   </div>
   <button id="cal-clear">Reset</button>
-    {% for key in ['type', 'lister', 'projet', 'url'] %}
+    {% for key in ['type', 'lister', 'project', 'url'] %}
         {% if origin[key] is not none %}
             <div class="row">
               <div class="col-md-2">{{ key }}</div>
diff --git a/swh/web/ui/tests/test_apidoc.py b/swh/web/ui/tests/test_apidoc.py
new file mode 100644
--- /dev/null
+++ b/swh/web/ui/tests/test_apidoc.py
@@ -0,0 +1,296 @@
+# Copyright (C) 2015  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+
+from unittest.mock import MagicMock, patch
+from nose.tools import istest
+
+from swh.web.ui import apidoc
+from swh.web.ui.tests import test_app
+
+
+class APIDocTestCase(test_app.SWHApidocTestCase):
+
+    def setUp(self):
+        self.arg_dict = {
+            'name': 'my_pretty_arg',
+            'default': 'some default value',
+            'type': 'str',
+            'doc': 'this arg does things'
+        }
+        self.stub_excs = [{'exc': 'catastrophic_exception',
+                           'doc': 'My exception documentation'}]
+        self.stub_args = [{'name': 'stub_arg',
+                           'default': 'some_default'}]
+        self.stub_rule_list = [
+            {'rule': 'some/route/with/args/',
+             'methods': {'GET', 'HEAD', 'OPTIONS'}},
+            {'rule': 'some/doc/route/',
+             'methods': {'GET', 'HEAD', 'OPTIONS'}},
+            {'rule': 'some/other/route/',
+             'methods': {'GET', 'HEAD', 'OPTIONS'}}
+        ]
+        self.stub_return = {
+            'type': 'some_return_type',
+            'doc': 'a dict with amazing properties'
+        }
+
+    @patch('swh.web.ui.apidoc.APIUrls')
+    @patch('swh.web.ui.apidoc.app')
+    @istest
+    def apidoc_route(self, mock_app, mock_api_urls):
+        # given
+        decorator = apidoc.route('/some/url/for/doc/')
+        mock_fun = MagicMock(return_value=123)
+        mock_fun.__doc__ = 'Some documentation'
+        mock_fun.__name__ = 'some_fname'
+        decorated = decorator.__call__(mock_fun)
+
+        # when
+        decorated('some', 'value', kws='and a kw')
+
+        # then
+        mock_fun.assert_called_once_with(
+            call_args=(('some', 'value'), {'kws': 'and a kw'}),
+            doc_route='/some/url/for/doc/',
+            noargs=False
+        )
+        mock_api_urls.index_add_route.assert_called_once_with(
+            '/some/url/for/doc/',
+            'Some documentation')
+        mock_app.add_url_rule.assert_called_once_with(
+            '/some/url/for/doc/', 'some_fname', decorated)
+
+    @istest
+    def apidoc_arg_noprevious(self):
+        # given
+        decorator = apidoc.arg('my_pretty_arg',
+                               default='some default value',
+                               argtype='str',
+                               argdoc='this arg does things')
+        mock_fun = MagicMock(return_value=123)
+        decorated = decorator.__call__(mock_fun)
+
+        # when
+        decorated(call_args=((), {}), doc_route='some/route/')
+
+        # then
+        mock_fun.assert_called_once_with(
+            call_args=((), {}),
+            doc_route='some/route/',
+            args=[self.arg_dict]
+        )
+
+    @istest
+    def apidoc_arg_previous(self):
+        # given
+        decorator = apidoc.arg('my_other_arg',
+                               default='some other value',
+                               argtype='str',
+                               argdoc='this arg is optional')
+        mock_fun = MagicMock(return_value=123)
+        decorated = decorator.__call__(mock_fun)
+
+        # when
+        decorated(call_args=((), {}),
+                  doc_route='some/route/',
+                  args=[self.arg_dict])
+
+        # then
+        mock_fun.assert_called_once_with(
+            call_args=((), {}),
+            doc_route='some/route/',
+            args=[self.arg_dict,
+                  {'name': 'my_other_arg',
+                   'default': 'some other value',
+                   'type': 'str',
+                   'doc': 'this arg is optional'}])
+
+    @istest
+    def apidoc_raises_noprevious(self):
+        # given
+        decorator = apidoc.raises(exc='catastrophic_exception',
+                                  doc='My exception documentation')
+        mock_fun = MagicMock(return_value=123)
+        decorated = decorator.__call__(mock_fun)
+
+        # when
+        decorated(call_args=((), {}), doc_route='some/route/')
+
+        # then
+        mock_fun.assert_called_once_with(
+            call_args=((), {}),
+            doc_route='some/route/',
+            excs=self.stub_excs
+        )
+
+    @istest
+    def apidoc_raises_previous(self):
+        # given
+        decorator = apidoc.raises(exc='cataclysmic_exception',
+                                  doc='Another documentation')
+        mock_fun = MagicMock(return_value=123)
+        decorated = decorator.__call__(mock_fun)
+        expected_excs = self.stub_excs + [{
+            'exc': 'cataclysmic_exception',
+            'doc': 'Another documentation'}]
+
+        # when
+        decorated(call_args=((), {}),
+                  doc_route='some/route/',
+                  excs=self.stub_excs)
+
+        # then
+        mock_fun.assert_called_once_with(
+            call_args=((), {}),
+            doc_route='some/route/',
+            excs=expected_excs)
+
+    @patch('swh.web.ui.apidoc.render_template')
+    @patch('swh.web.ui.apidoc.url_for')
+    @patch('swh.web.ui.apidoc.APIUrls')
+    @patch('swh.web.ui.apidoc.request')
+    @istest
+    def apidoc_returns_doc_call(self,
+                                mock_request,
+                                mock_api_urls,
+                                mock_url_for,
+                                mock_render):
+        # given
+        decorator = apidoc.returns(rettype='some_return_type',
+                                   retdoc='a dict with amazing properties')
+        mock_fun = MagicMock(return_value=123)
+        mock_fun.__name__ = 'some_fname'
+        mock_fun.__doc__ = 'Some documentation'
+        decorated = decorator.__call__(mock_fun)
+
+        mock_api_urls.get_method_endpoints.return_value = self.stub_rule_list
+
+        mock_request.url = 'http://my-domain.tld/some/doc/route/'
+        mock_url_for.return_value = 'http://my-domain.tld/meaningful_route/'
+
+        expected_env = {
+            'urls': [{'rule': 'some/route/with/args/',
+                      'methods': {'GET', 'HEAD', 'OPTIONS'}},
+                     {'rule': 'some/other/route/',
+                      'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+            'docstring': 'Some documentation',
+            'args': self.stub_args,
+            'excs': self.stub_excs,
+            'route': 'some/doc/route/',
+            'example': 'http://my-domain.tld/meaningful_route/',
+            'return': self.stub_return
+        }
+
+        # when
+        decorated(
+            docstring='Some documentation',
+            call_args=(('some', 'args'), {'kw': 'kwargs'}),
+            args=self.stub_args,
+            excs=self.stub_excs,
+            doc_route='some/doc/route/',
+            noargs=False
+        )
+
+        # then
+        self.assertEqual(mock_fun.call_args_list, [])  # function not called
+        mock_render.assert_called_once_with(
+            'apidoc.html',
+            **expected_env
+        )
+
+    @patch('swh.web.ui.apidoc.g')
+    @patch('swh.web.ui.apidoc.url_for')
+    @patch('swh.web.ui.apidoc.APIUrls')
+    @patch('swh.web.ui.apidoc.request')
+    @istest
+    def apidoc_returns_noargs(self,
+                              mock_request,
+                              mock_api_urls,
+                              mock_url_for,
+                              mock_g):
+
+        # given
+        decorator = apidoc.returns(rettype='some_return_type',
+                                   retdoc='a dict with amazing properties')
+        mock_fun = MagicMock(return_value=123)
+        mock_fun.__name__ = 'some_fname'
+        mock_fun.__doc__ = 'Some documentation'
+        decorated = decorator.__call__(mock_fun)
+
+        mock_api_urls.get_method_endpoints.return_value = [
+            {'rule': 'some/doc/route/',
+             'methods': {'GET', 'HEAD', 'OPTIONS'}}]
+        mock_request.url = 'http://my-domain.tld/some/doc/route/'
+        doc_dict = {
+            'urls': [
+                {'rule': 'some/doc/route/',
+                 'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+            'docstring': 'Some documentation',
+            'route': 'some/doc/route/',
+            'return': {'type': 'some_return_type',
+                       'doc': 'a dict with amazing properties'}
+        }
+
+        # when
+        decorated(
+            call_args=((), {}),
+            doc_route='some/doc/route/',
+            noargs=True
+        )
+
+        # then
+        mock_fun.assert_called_once_with()
+        self.assertEqual(mock_g.doc_env, doc_dict)
+
+    @patch('swh.web.ui.apidoc.g')
+    @patch('swh.web.ui.apidoc.url_for')
+    @patch('swh.web.ui.apidoc.APIUrls')
+    @patch('swh.web.ui.apidoc.request')
+    @istest
+    def apidoc_return_endpoint_call(self,
+                                    mock_request,
+                                    mock_api_urls,
+                                    mock_url_for,
+                                    mock_g):
+        # given
+        decorator = apidoc.returns(rettype='some_return_type',
+                                   retdoc='a dict with amazing properties')
+        mock_fun = MagicMock(return_value=123)
+        mock_fun.__name__ = 'some_fname'
+        mock_fun.__doc__ = 'Some documentation'
+        decorated = decorator.__call__(mock_fun)
+
+        mock_api_urls.get_method_endpoints.return_value = self.stub_rule_list
+
+        mock_request.url = 'http://my-domain.tld/some/arg/route/'
+        mock_url_for.return_value = 'http://my-domain.tld/some/arg/route'
+
+        doc_dict = {
+            'urls': [{'rule': 'some/route/with/args/',
+                      'methods': {'GET', 'HEAD', 'OPTIONS'}},
+                     {'rule': 'some/other/route/',
+                      'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+            'docstring': 'Some documentation',
+            'args': self.stub_args,
+            'excs': self.stub_excs,
+            'route': 'some/doc/route/',
+            'example': 'http://my-domain.tld/some/arg/route',
+            'return': self.stub_return
+        }
+
+        # when
+        decorated(
+            docstring='Some documentation',
+            call_args=(('some', 'args'), {'kw': 'kwargs'}),
+            args=self.stub_args,
+            excs=self.stub_excs,
+            noargs=False,
+            doc_route='some/doc/route/',
+        )
+
+        # then
+        mock_fun.assert_called_once_with('some', 'args', kw='kwargs')
+        self.assertEqual(mock_g.doc_env, doc_dict)
diff --git a/swh/web/ui/tests/test_app.py b/swh/web/ui/tests/test_app.py
--- a/swh/web/ui/tests/test_app.py
+++ b/swh/web/ui/tests/test_app.py
@@ -60,6 +60,16 @@
     return main.app.test_client(), main.app.config, storage, main.app
 
 
+class SWHApidocTestCase(unittest.TestCase):
+    """Testing APIDoc class.
+
+    """
+    @classmethod
+    def setUpClass(cls):
+        cls.app, cls.app_config, cls.storage, _ = create_app()
+        cls.maxDiff = None
+
+
 class SWHApiTestCase(unittest.TestCase):
     """Testing API class.
 
diff --git a/swh/web/ui/tests/test_backend.py b/swh/web/ui/tests/test_backend.py
--- a/swh/web/ui/tests/test_backend.py
+++ b/swh/web/ui/tests/test_backend.py
@@ -532,12 +532,12 @@
         self.storage.revision_log = MagicMock(return_value=stub_revision_log)
 
         # when
-        actual_revision = backend.revision_log(sha1_bin)
+        actual_revision = backend.revision_log(sha1_bin, limit=1)
 
         # then
         self.assertEqual(list(actual_revision), stub_revision_log)
 
-        self.storage.revision_log.assert_called_with([sha1_bin], 100)
+        self.storage.revision_log.assert_called_with([sha1_bin], 1)
 
     @istest
     def revision_log_by(self):
@@ -571,11 +571,12 @@
             return_value=stub_revision_log)
 
         # when
-        actual_log = backend.revision_log_by(1, 'refs/heads/master', None)
+        actual_log = backend.revision_log_by(1, 'refs/heads/master',
+                                             None, limit=1)
 
         # then
         self.assertEqual(actual_log, stub_revision_log)
-        self.storage.revision_log.assert_called_with([sha1_bin], 100)
+        self.storage.revision_log.assert_called_with([sha1_bin], 1)
 
     @istest
     def revision_log_by_norev(self):
@@ -586,11 +587,12 @@
         self.storage.revision_log_by = MagicMock(return_value=None)
 
         # when
-        actual_log = backend.revision_log_by(1, 'refs/heads/master', None)
+        actual_log = backend.revision_log_by(1, 'refs/heads/master',
+                                             None, limit=1)
 
         # then
         self.assertEqual(actual_log, None)
-        self.storage.revision_log.assert_called_with([sha1_bin], 100)
+        self.storage.revision_log.assert_called_with([sha1_bin], 1)
 
     @istest
     def stat_counters(self):
diff --git a/swh/web/ui/tests/test_renderers.py b/swh/web/ui/tests/test_renderers.py
--- a/swh/web/ui/tests/test_renderers.py
+++ b/swh/web/ui/tests/test_renderers.py
@@ -7,15 +7,140 @@
 import unittest
 import yaml
 
+from flask import Response
 from flask_api.mediatypes import MediaType
 from nose.tools import istest
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
 
 from swh.web.ui import renderers
 
 
 class RendererTestCase(unittest.TestCase):
 
+    @patch('swh.web.ui.renderers.g')
+    @patch('swh.web.ui.renderers.json')
+    @patch('swh.web.ui.renderers.request')
+    @patch('swh.web.ui.renderers.render_template')
+    @patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
+    @istest
+    def swh_multi_response_mimetype_html(self, mock_filter, mock_render,
+                                         mock_request, mock_json, mock_g):
+        # given
+        data = {'data': [12, 34],
+                'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'}
+        mock_g.get.return_value = {'my_key': 'my_display_value'}
+        mock_filter.return_value = data
+        expected_env = {
+            'my_key': 'my_display_value',
+            'response_data': json.dumps(data),
+            'request': mock_request
+        }
+
+        def mock_mimetypes(key):
+            mimetypes = {
+                'text/html': 10,
+                'application/json': 0.1,
+                'application/yaml': 0.1
+            }
+            return mimetypes[key]
+        accept_mimetypes = MagicMock()
+        accept_mimetypes.__getitem__.side_effect = mock_mimetypes
+        accept_mimetypes.best_match = MagicMock(return_value='text/html')
+        mock_request.accept_mimetypes = accept_mimetypes
+        mock_json.dumps.return_value = json.dumps(data)
+
+        # when
+        rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
+
+        # then
+        mock_filter.assert_called_once_with(renderers.SWHMultiResponse, data)
+        mock_render.assert_called_with('apidoc.html', **expected_env)
+        self.assertEqual(rv.status_code, 200)
+        self.assertEqual(rv.mimetype, 'text/html')
+
+    @patch('swh.web.ui.renderers.g')
+    @patch('swh.web.ui.renderers.yaml')
+    @patch('swh.web.ui.renderers.request')
+    @patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
+    @istest
+    def swh_multi_response_mimetype_yaml(self, mock_filter,
+                                         mock_request, mock_yaml, mock_g):
+        # given
+        data = {'data': [12, 34],
+                'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'}
+
+        def mock_mimetypes(key):
+            mimetypes = {
+                'application/yaml': 10,
+                'application/json': 0.1,
+                'text/html': 0.1
+            }
+            return mimetypes[key]
+        accept_mimetypes = MagicMock()
+        accept_mimetypes.__getitem__.side_effect = mock_mimetypes
+        accept_mimetypes.best_match = MagicMock(
+            return_value='application/yaml')
+        mock_request.accept_mimetypes = accept_mimetypes
+        mock_yaml.dump.return_value = yaml.dump(data)
+        mock_filter.return_value = data
+
+        # when
+        rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
+
+        # then
+        mock_filter.assert_called_once_with(renderers.SWHMultiResponse, data)
+        mock_yaml.dump.assert_called_once_with(data)
+        self.assertEqual(rv.status_code, 200)
+        self.assertEqual(rv.mimetype, 'application/yaml')
+        self.assertEqual(data, yaml.load(rv.data.decode('utf-8')))
+
+    @patch('swh.web.ui.renderers.g')
+    @patch('swh.web.ui.renderers.json')
+    @patch('swh.web.ui.renderers.request')
+    @patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
+    @istest
+    def swh_multi_response_mimetype_json(self, mock_filter,
+                                         mock_request, mock_json, mock_g):
+        # given
+        data = {'data': [12, 34],
+                'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'}
+
+        def mock_mimetypes(key):
+            mimetypes = {
+                'application/json': 10,
+                'text/html': 0.1,
+                'application/yaml': 0.1
+            }
+            return mimetypes[key]
+        accept_mimetypes = MagicMock()
+        accept_mimetypes.__getitem__.side_effect = mock_mimetypes
+        accept_mimetypes.best_match = MagicMock(
+            return_value='application/json')
+        mock_request.accept_mimetypes = accept_mimetypes
+        mock_json.dumps.return_value = json.dumps(data)
+        mock_filter.return_value = data
+
+        # when
+        rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
+
+        # then
+        mock_filter.assert_called_once_with(renderers.SWHMultiResponse, data)
+        mock_json.dumps.assert_called_once_with(data)
+        self.assertEqual(rv.status_code, 200)
+        self.assertEqual(rv.mimetype, 'application/json')
+        self.assertEqual(data, json.loads(rv.data.decode('utf-8')))
+
+    @istest
+    def apidoc_make_response_not_list_dict(self):
+        # given
+        incoming = Response()
+
+        # when
+        rv = renderers.SWHMultiResponse.make_response_from_mimetype(incoming)
+
+        # then
+        self.assertEqual(rv, incoming)
+
     @patch('swh.web.ui.renderers.request')
     @istest
     def swh_filter_renderer_do_nothing(self, mock_request):
@@ -200,6 +325,14 @@
                           other_content)
 
     @istest
+    def revision_id_from_url(self):
+        url = ('/browse/revision/9ba4bcb645898d562498ea66a0df958ef0e7a68c/'
+               'prev/9ba4bcb645898d562498ea66a0df958ef0e7aaaa/')
+
+        expected_id = '9ba4bcb645898d562498ea66a0df958ef0e7a68c'
+        self.assertEqual(renderers.revision_id_from_url(url), expected_id)
+
+    @istest
     def safe_docstring_display(self):
         # update api link with html links content with links
         docstring = """<p>Show all revisions (~git log) starting from
diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py
--- a/swh/web/ui/tests/test_service.py
+++ b/swh/web/ui/tests/test_service.py
@@ -1186,7 +1186,8 @@
 
         # when
         actual_revision = service.lookup_revision_log(
-            'abcdbe353ed3480476f032475e7c233eff7371d5')
+            'abcdbe353ed3480476f032475e7c233eff7371d5',
+            limit=25)
 
         # then
         self.assertEqual(list(actual_revision), [self.SAMPLE_REVISION])
diff --git a/swh/web/ui/tests/views/test_browse.py b/swh/web/ui/tests/views/test_browse.py
--- a/swh/web/ui/tests/views/test_browse.py
+++ b/swh/web/ui/tests/views/test_browse.py
@@ -20,6 +20,26 @@
 class SearchView(test_app.SWHViewTestCase):
     render_template = False
 
+    @patch('swh.web.ui.apidoc.APIUrls')
+    @istest
+    def browse_api_doc(self, mock_api_urls):
+        # given
+        endpoints = {
+            '/a/doc/endpoint/': 'relevant documentation',
+            '/some/other/endpoint/': 'more docstrings'}
+        mock_api_urls.apidoc_routes = endpoints
+
+        # when
+        rv = self.client.get('/api/1/doc/')
+
+        # then
+        self.assertEquals(rv.status_code, 200)
+        self.assertIsNotNone(
+            self.get_context_variable('doc_routes'),
+            sorted(endpoints.items())
+        )
+        self.assert_template_used('api.html')
+
     @istest
     def search_default(self):
         # when
diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py
--- a/swh/web/ui/views/api.py
+++ b/swh/web/ui/views/api.py
@@ -5,32 +5,36 @@
 
 from types import GeneratorType
 
-from flask import request, url_for, Response, redirect
+from flask import request, url_for
 
-from swh.web.ui import service, utils
+from swh.web.ui import service, utils, apidoc as doc
 from swh.web.ui.exc import NotFoundExc
 from swh.web.ui.main import app
 
 
 @app.route('/api/1/stat/counters/')
+@doc.route('/api/1/stat/counters/', noargs=True)
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="A dictionary of SWH's most important statistics")
 def api_stats():
     """Return statistics on SWH storage.
 
-    Returns:
-        SWH storage's statistics.
-
     """
     return service.stat_counters()
 
 
 @app.route('/api/1/stat/visits/<int:origin_id>/')
+@doc.route('/api/1/stat/visits/')
+@doc.arg('origin_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc='The requested SWH origin identifier')
+@doc.returns(rettype=doc.rettypes.list,
+             retdoc="""All instances of visits of the origin pointed by
+             origin_id as POSIX time since epoch""")
 def api_origin_visits(origin_id):
-    """Return visit dates for the given revision.
-
-    Returns:
-        A list of SWH visit occurrence timestamps, sorted from oldest to
-        newest.
-
+    """Return a list of visit dates as POSIX timestamps for the
+    given revision.
     """
     date_gen = (item['date'] for item in service.stat_origin_visits(origin_id))
     return sorted(date_gen)
@@ -38,22 +42,27 @@
 
 @app.route('/api/1/search/', methods=['POST'])
 @app.route('/api/1/search/<string:q>/')
+@doc.route('/api/1/search/')
+@doc.arg('q',
+         default='sha1:adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
+         argtype=doc.argtypes.algo_and_hash,
+         argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
+         sha1_git or sha256 and hash is the hash to search for in SWH""")
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if q is not well formed')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""A dict with keys:
+               * search_res: a list of dicts corresponding to queried content
+                 with key 'found' to True if found, 'False' if not
+               * search_stats: a dict containing number of files searched and
+                 percentage of files found
+             """)
 def api_search(q=None):
     """Search a content per hash.
 
-    Args:
-        q is of the form algo_hash:hash with algo_hash in
-        (sha1, sha1_git, sha256).
-
-    Returns:
-        Dictionary with 'found' key and the associated result.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-
-    Example:
-        GET /api/1/search/sha1:bd819b5b28fcde3bf114d16a44ac46250da94ee5/
-
+    This may take the form of a GET request with a single checksum, or a POST
+    request with many hashes, with the request body containing identifiers
+    (typically filenames) as keys and corresponding hashes as values.
     """
 
     response = {'search_res': None,
@@ -139,71 +148,56 @@
     return enrich_fn(res)
 
 
-@app.route('/api/1/origin/')
 @app.route('/api/1/origin/<int:origin_id>/')
+@doc.route('/api/1/origin/')
+@doc.arg('origin_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc="The origin's SWH origin_id.")
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if origin_id does not correspond to an origin in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc='The metadata of the origin identified by origin_id')
 def api_origin(origin_id):
     """Return information about origin with id origin_id.
-
-
-    Args:
-        origin_id: the origin's identifier.
-
-    Returns:
-        Information on the origin if found.
-
-    Raises:
-        NotFoundExc if the origin is not found.
-
-    Example:
-        GET /api/1/origin/1/
-
     """
     return _api_lookup(
         origin_id, lookup_fn=service.lookup_origin,
         error_msg_if_not_found='Origin with id %s not found.' % origin_id)
 
 
-@app.route('/api/1/person/')
 @app.route('/api/1/person/<int:person_id>/')
+@doc.route('/api/1/person/')
+@doc.arg('person_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc="The person's SWH identifier")
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if person_id does not correspond to an origin in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc='The metadata of the person identified by person_id')
 def api_person(person_id):
     """Return information about person with identifier person_id.
-
-    Args:
-        person_id: the person's identifier.
-
-    Returns:
-        Information on the person if found.
-
-    Raises:
-        NotFoundExc if the person is not found.
-
-    Example:
-        GET /api/1/person/1/
-
     """
     return _api_lookup(
         person_id, lookup_fn=service.lookup_person,
         error_msg_if_not_found='Person with id %s not found.' % person_id)
 
 
-@app.route('/api/1/release/')
 @app.route('/api/1/release/<string:sha1_git>/')
+@doc.route('/api/1/release/')
+@doc.arg('sha1_git',
+         default='8b137891791fe96927ad78e64b0aad7bded08bdc',
+         argtype=doc.argtypes.sha1_git,
+         argdoc="The release's sha1_git identifier")
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if the argument is not a sha1')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if sha1_git does not correspond to a release in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc='The metadata of the release identified by sha1_git')
 def api_release(sha1_git):
     """Return information about release with id sha1_git.
-
-    Args:
-        sha1_git: the release's hash.
-
-    Returns:
-        Information on the release if found.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the release is not found.
-
-    Example:
-        GET /api/1/release/b307094f00c3641b0c9da808d894f3a325371414
-
     """
     error_msg = 'Release with sha1_git %s not found.' % sha1_git
     return _api_lookup(
@@ -267,6 +261,31 @@
            '/branch/<path:branch_name>'
            '/ts/<string:ts>'
            '/directory/<path:path>/')
+@doc.route('/api/1/revision/origin/directory/')
+@doc.arg('origin_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc="The revision's origin's SWH identifier")
+@doc.arg('branch_name',
+         default='refs/heads/master',
+         argtype=doc.argtypes.path,
+         argdoc="""The optional branch for the given origin (default
+         to master""")
+@doc.arg('ts',
+         default='2000-01-17T11:23:54+00:00',
+         argtype=doc.argtypes.ts,
+         argdoc="""Optional timestamp (default to the nearest time
+         crawl of timestamp)""")
+@doc.arg('path',
+         default='.',
+         argtype=doc.argtypes.path,
+         argdoc='The path to the directory or file to display')
+@doc.raises(exc=doc.excs.notfound,
+            doc="""Raised if a revision matching the passed criteria was
+            not found""")
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata of the revision corresponding to the
+             passed criteria""")
 def api_directory_through_revision_origin(origin_id,
                                           branch_name="refs/heads/master",
                                           ts=None,
@@ -274,24 +293,6 @@
                                           with_data=False):
     """Display directory or content information through a revision identified
     by origin/branch/timestamp.
-
-    Args:
-        origin_id: origin's identifier (default to 1).
-        branch_name: the optional branch for the given origin (default
-        to master).
-        timestamp: optional timestamp (default to the nearest time
-        crawl of timestamp).
-        path: Path to directory or file to display.
-        with_data: indicate to retrieve the content's raw data if path resolves
-        to a content.
-
-    Returns:
-        Information on the directory or content pointed to by such revision.
-
-    Raises:
-        NotFoundExc if the revision is not found or the path pointed to
-        is not found.
-
     """
     if ts:
         ts = utils.parse_timestamp(ts)
@@ -319,28 +320,31 @@
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/ts/<string:ts>/')
+@doc.route('/api/1/revision/origin/')
+@doc.arg('origin_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc="The queried revision's origin identifier in SWH")
+@doc.arg('branch_name',
+         default='refs/heads/master',
+         argtype=doc.argtypes.path,
+         argdoc="""The optional branch for the given origin (default
+         to master)""")
+@doc.arg('ts',
+         default='2000-01-17T11:23:54+00:00',
+         argtype=doc.argtypes.ts,
+         argdoc="The time at which the queried revision should be constrained")
+@doc.raises(exc=doc.excs.notfound,
+            doc="""Raised if a revision matching given criteria was not found
+            in SWH""")
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata of the revision identified by the given
+             criteria""")
 def api_revision_with_origin(origin_id,
                              branch_name="refs/heads/master",
                              ts=None):
-    """Instead of having to specify a (root) revision by SHA1_GIT, users
-    might want to specify a place and a time. In SWH a "place" is an
-    origin; a "time" is a timestamp at which some place has been
-    observed by SWH crawlers.
-
-    Args:
-        origin_id: origin's identifier (default to 1).
-        branch_name: the optional branch for the given origin (default
-        to master).
-        timestamp: optional timestamp (default to the nearest time
-        crawl of timestamp).
-
-    Returns:
-        Information on the revision if found.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the revision is not found.
-
+    """Display revision information through its identification by
+    origin/branch/timestamp.
     """
     if ts:
         ts = utils.parse_timestamp(ts)
@@ -357,24 +361,25 @@
         ts)
 
 
-@app.route('/api/1/revision/')
 @app.route('/api/1/revision/<string:sha1_git>/')
 @app.route('/api/1/revision/<string:sha1_git>/prev/<path:context>/')
+@doc.route('/api/1/revision/')
+@doc.arg('sha1_git',
+         default='ec72c666fb345ea5f21359b7bc063710ce558e39',
+         argtype=doc.argtypes.sha1_git,
+         argdoc="The revision's sha1_git identifier")
+@doc.arg('context',
+         default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a',
+         argtype=doc.argtypes.path,
+         argdoc='The navigation breadcrumbs -- use at your own risk')
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if sha1_git is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a revision matching sha1_git was not found in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc='The metadata of the revision identified by sha1_git')
 def api_revision(sha1_git, context=None):
     """Return information about revision with id sha1_git.
-
-    Args:
-        sha1_git: the revision's hash.
-
-    Returns:
-        Information on the revision if found.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the revision is not found.
-
-    Example:
-        GET /api/1/revision/baf18f9fc50a0b6fef50460a76c33b2ddc57486e
     """
     def _enrich_revision(revision, context=context):
         return utils.enrich_revision(revision, context)
@@ -387,58 +392,53 @@
 
 
 @app.route('/api/1/revision/<string:sha1_git>/raw/')
+@doc.route('/api/1/revision/raw/')
+@doc.arg('sha1_git',
+         default='ec72c666fb345ea5f21359b7bc063710ce558e39',
+         argtype=doc.argtypes.sha1_git,
+         argdoc="The queried revision's sha1_git identifier")
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if sha1_git is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a revision matching sha1_git was not found in SWH')
+@doc.returns(rettype=doc.argtypes.octet_stream,
+             retdoc="""The message of the revision identified by sha1_git
+             as a downloadable octet stream""")
 def api_revision_raw_message(sha1_git):
-    """Return the raw data of the revision's message
-
-    Args:
-        sha1_git: the revision's hash
-
-    Returns:
-        The raw revision message, possibly in an illegible
-        format for humans, decoded in utf-8 by default.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the revision is not found or the revision has no
-        message
-
-    Example:
-        GET /api/1/revision/baf18f9fc50a0b6fef50460a76c33b2ddc57486e/raw/
-
+    """Return the raw data of the message of revision identified by sha1_git
     """
     raw = service.lookup_revision_message(sha1_git)
-    return Response(raw['message'],
-                    headers={'Content-disposition': 'attachment;'
-                             'filename=rev_%s_raw' % sha1_git},
-                    mimetype='application/octet-stream')
+    return app.response_class(raw['message'],
+                              headers={'Content-disposition': 'attachment;'
+                                       'filename=rev_%s_raw' % sha1_git},
+                              mimetype='application/octet-stream')
 
 
 @app.route('/api/1/revision/<string:sha1_git>/directory/')
 @app.route('/api/1/revision/<string:sha1_git>/directory/<path:dir_path>/')
+@doc.route('/api/1/revision/directory/')
+@doc.arg('sha1_git',
+         default='ec72c666fb345ea5f21359b7bc063710ce558e39',
+         argtype=doc.argtypes.sha1_git,
+         argdoc="The revision's sha1_git identifier.")
+@doc.arg('dir_path',
+         default='.',
+         argtype=doc.argtypes.path,
+         argdoc='The path from the top level directory')
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if sha1_git is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc="""Raised if a revision matching sha1_git was not found in SWH
+            , or if the path specified does not exist""")
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata of the directory pointed by revision id
+             sha1-git and dir_path""")
 def api_revision_directory(sha1_git,
                            dir_path=None,
                            with_data=False):
     """Return information on directory pointed by revision with sha1_git.
     If dir_path is not provided, display top level directory.
     Otherwise, display the directory pointed by dir_path (if it exists).
-
-    Args:
-        sha1_git: revision's hash.
-        dir_path: optional directory pointed to by that revision.
-        with_data: indicate to retrieve the content's raw data if path resolves
-        to a content
-
-    Returns:
-        Information on the directory pointed to by that revision.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc either if the revision is not found or the path referenced
-        does not exist
-
-    Example:
-        GET /api/1/revision/baf18f9fc50a0b6fef50460a76c33b2ddc57486e/directory/
-
     """
     return _revision_directory_by(
         {
@@ -451,23 +451,27 @@
 
 @app.route('/api/1/revision/<string:sha1_git>/log/')
 @app.route('/api/1/revision/<string:sha1_git>/prev/<path:prev_sha1s>/log/')
+@doc.route('/api/1/revision/log/')
+@doc.arg('sha1_git',
+         default='ec72c666fb345ea5f21359b7bc063710ce558e39',
+         argtype=doc.argtypes.sha1_git,
+         argdoc='The sha1_git of the revision queried')
+@doc.arg('prev_sha1s',
+         default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a',
+         argtype=doc.argtypes.path,
+         argdoc='The navigation breadcrumbs -- use at your own risk!')
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if sha1_git or prev_sha1s is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a revision matching sha1_git was not found in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The log data starting at the revision identified by
+             sha1_git, completed with the navigation breadcrumbs,
+             if any""")
 def api_revision_log(sha1_git, prev_sha1s=None):
     """Show all revisions (~git log) starting from sha1_git.
-       The first element returned is the given sha1_git.
-
-    Args:
-        sha1_git: the revision's hash.
-        prev_sha1s: the navigation breadcrumb
-        limit: optional query parameter to limit the revisions log
-        (default to 100).
-
-    Returns:
-        Information on the revision if found, complemented with the revision's
-        children if we have navigation breadcrumbs for them.
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the revision is not found.
+    The first element returned is the given sha1_git, or the first
+    breadcrumb, if any.
 
     """
     limit = app.config['conf']['max_log_revs']
@@ -510,8 +514,6 @@
 
 
 @app.route('/api/1/revision'
-           '/origin/log/')
-@app.route('/api/1/revision'
            '/origin/<int:origin_id>/log/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
@@ -523,26 +525,32 @@
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/ts/<string:ts>/log/')
+@doc.route('/api/1/revision/origin/log/')
+@doc.arg('origin_id',
+         default=1,
+         argtype=doc.argtypes.int,
+         argdoc="The revision's SWH origin identifier")
+@doc.arg('branch_name',
+         default='refs/heads/master',
+         argtype=doc.argtypes.path,
+         argdoc="The revision's branch name within the origin specified")
+@doc.arg('ts',
+         default='2000-01-17T11:23:54+00:00',
+         argtype=doc.argtypes.ts,
+         argdoc="""A time or timestamp string to parse""")
+@doc.raises(exc=doc.excs.notfound,
+            doc="""Raised if a revision matching the given criteria was not
+            found in SWH""")
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata of the revision log starting at the revision
+             matching the given criteria.""")
 def api_revision_log_by(origin_id,
                         branch_name='refs/heads/master',
                         ts=None):
     """Show all revisions (~git log) starting from the revision
-       described by its origin_id, optional branch name and timestamp.
-       The first element returned is the described revision.
-
-    Args:
-        origin_id: the revision's origin.
-        branch_name: the branch of the revision (optional, defaults to
-        master
-        ts: the requested timeframe near which the revision was created.
-        limit: optional query parameter to limit the revisions log
-        (default to 100).
-
-    Returns:
-        Information on the revision log if found.
+    described by its origin_id, optional branch name and timestamp.
+    The first element returned is the described revision.
 
-    Raises:
-        NotFoundExc if the revision is not found.
     """
     limit = app.config['conf']['max_log_revs']
     response = {'revisions': None, 'next_revs_url': None}
@@ -576,26 +584,28 @@
     return response
 
 
-@app.route('/api/1/directory/')
 @app.route('/api/1/directory/<string:sha1_git>/')
 @app.route('/api/1/directory/<string:sha1_git>/<path:path>/')
+@doc.route('/api/1/directory/')
+@doc.arg('sha1_git',
+         default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
+         argtype=doc.argtypes.sha1_git,
+         argdoc="The queried directory's corresponding sha1_git hash")
+@doc.arg('path',
+         default='.',
+         argtype=doc.argtypes.path,
+         argdoc="A path relative to the queried directory's top level")
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if sha1_git is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a directory matching sha1_git was not found in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata and contents of the release identified by
+             sha1_git""")
 def api_directory(sha1_git,
                   path=None):
     """Return information about release with id sha1_git.
 
-    Args:
-        sha1_git: Directory's sha1_git. If path exists: starting directory for
-        relative navigation.
-        path: The path to the queried directory
-
-    Raises:
-        BadInputExc in case of unknown algo_hash or bad hash.
-        NotFoundExc if the content is not found.
-
-    Example:
-        GET /api/1/directory/8d7dc91d18546a91564606c3e3695a5ab568d179
-        GET /api/1/directory/8d7dc91d18546a91564606c3e3695a5ab568d179/path/dir/
-
     """
     if path:
         error_msg_path = ('Entry with path %s relative to directory '
@@ -644,21 +654,23 @@
 
 
 @app.route('/api/1/content/<string:q>/raw/')
+@doc.route('/api/1/content/raw/')
+@doc.arg('q',
+         default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
+         argtype=doc.argtypes.algo_and_hash,
+         argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
+         sha1_git or sha256 and hash is the hash to search for in SWH. Defaults
+         to sha1 in the case of a missing algo_hash
+         """)
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if q is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a content matching q was not found in SWH')
+@doc.returns(rettype='octet stream',
+             retdoc='The raw content data as an octet stream')
 def api_content_raw(q):
     """Return content's raw data if content is found.
 
-    Args:
-        q is of the form (algo_hash:)hash with algo_hash in
-        (sha1, sha1_git, sha256).
-        When algo_hash is not provided, 'hash' is considered sha1.
-
-    Returns:
-        Content's raw data in application/octet-stream.
-
-    Raises:
-        - BadInputExc in case of unknown algo_hash or bad hash
-        - NotFoundExc if the content is not found.
-
     """
     def generate(content):
         yield content['data']
@@ -667,30 +679,31 @@
     if not content:
         raise NotFoundExc('Content with %s not found.' % q)
 
-    return Response(generate(content), mimetype='application/octet-stream')
+    return app.response_class(generate(content),
+                              headers={'Content-disposition': 'attachment;'
+                                       'filename=content_%s_raw' % q},
+                              mimetype='application/octet-stream')
 
 
-@app.route('/api/1/content/')
 @app.route('/api/1/content/<string:q>/')
+@doc.route('/api/1/content/')
+@doc.arg('q',
+         default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
+         argtype=doc.argtypes.algo_and_hash,
+         argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
+         sha1_git or sha256 and hash is the hash to search for in SWH. Defaults
+         to sha1 in the case of a missing algo_hash
+         """)
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if q is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if a content matching q was not found in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc="""The metadata of the content identified by q. If content
+             decoding was successful, it also returns the data""")
 def api_content_metadata(q):
     """Return content information if content is found.
 
-    Args:
-        q is of the form (algo_hash:)hash with algo_hash in
-        (sha1, sha1_git, sha256).
-        When algo_hash is not provided, 'hash' is considered sha1.
-
-    Returns:
-        Content's information.
-
-    Raises:
-        - BadInputExc in case of unknown algo_hash or bad hash.
-        - NotFoundExc if the content is not found.
-
-    Example:
-        GET /api/1/content/sha256:e2c76e40866bb6b28916387bdfc8649beceb
-                                  523015738ec6d4d540c7fe65232b
-
     """
     return _api_lookup(
         q,
@@ -699,27 +712,21 @@
         enrich_fn=utils.enrich_content)
 
 
-@app.route('/api/1/entity/')
 @app.route('/api/1/entity/<string:uuid>/')
+@doc.route('/api/1/entity/')
+@doc.arg('uuid',
+         default='5f4d4c51-498a-4e28-88b3-b3e4e8396cba',
+         argtype=doc.argtypes.uuid,
+         argdoc="The entity's uuid identifier")
+@doc.raises(exc=doc.excs.badinput,
+            doc='Raised if uuid is not well formed')
+@doc.raises(exc=doc.excs.notfound,
+            doc='Raised if an entity matching uuid was not found in SWH')
+@doc.returns(rettype=doc.rettypes.dict,
+             retdoc='The metadata of the entity identified by uuid')
 def api_entity_by_uuid(uuid):
     """Return content information if content is found.
 
-    Args:
-        q is of the form (algo_hash:)hash with algo_hash in
-        (sha1, sha1_git, sha256).
-        When algo_hash is not provided, 'hash' is considered sha1.
-
-    Returns:
-        Content's information.
-
-    Raises:
-        - BadInputExc in case of unknown algo_hash or bad hash.
-        - NotFoundExc if the content is not found.
-
-    Example:
-        - GET /api/1/entity/5f4d4c51-498a-4e28-88b3-b3e4e8396cba/
-        - GET /api/1/entity/7c33636b-8f11-4bda-89d9-ba8b76a42cec/
-
     """
     return _api_lookup(
         uuid,
diff --git a/swh/web/ui/views/browse.py b/swh/web/ui/views/browse.py
--- a/swh/web/ui/views/browse.py
+++ b/swh/web/ui/views/browse.py
@@ -10,7 +10,7 @@
 from flask.ext.api.renderers import HTMLRenderer
 
 from swh.core.hashutil import ALGORITHMS
-from .. import service, utils
+from .. import service, utils, apidoc
 from ..exc import BadInputExc, NotFoundExc
 from ..main import app
 from . import api
@@ -18,6 +18,19 @@
 hash_filter_keys = ALGORITHMS
 
 
+@app.route('/api/1/doc/')
+@set_renderers(HTMLRenderer)
+def api_doc():
+    """Render the API's documentation.
+    """
+    routes = apidoc.APIUrls.get_app_endpoints()
+    # Return a list of routes with consistent ordering
+    env = {
+        'doc_routes': sorted(routes.items())
+    }
+    return render_template('api.html', **env)
+
+
 @app.route('/search/', methods=['GET', 'POST'])
 @set_renderers(HTMLRenderer)
 def search():